package connection;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;

import utils.Serialization;
import connection.exception.ConnectionResetException;

/**
 * The Class SocketTCP creates an abstraction to send/receive objects over TCP. That class 
 * manages all the initializations and the marshal needed to send and the unmarshal to receive the
 * messages. That operation of converting the messages is made by a serialization class.
 */
class TCPSocket {

	/** The Socket instance over witch the data will be sent. */
	private Socket _socketTcp;

	/** The inputStream used to receive data. */
	private DataInputStream _in;

	/** The outputStream used to send data. */
	private DataOutputStream _out;

	/** Defines the maximum size of a message. */
	private static final int _segmentSize = 64 * 1028;

	/**The amount of time that the socket will be blocked waiting for receive data*/
	private int _timeout = 40000;

	/**
	 * Instantiates a new SocketTcp object and create the input and output streams to the given socket.
	 * That method is used on the server side when creating a SocketTCP with the socket returned by the
	 * accept method.
	 * 
	 * @param socket the socket that will the used to the communications.
	 */
	protected TCPSocket(Socket socket){
		_socketTcp = socket;

		try {
			_in = new DataInputStream(_socketTcp.getInputStream());
			_out = new DataOutputStream(_socketTcp.getOutputStream());
		} catch (IOException e) {
			System.out.println("Exception thrown creating the I\\O Streams: "+e.getMessage());
			e.printStackTrace();
		}		
	}

	/**
	 * Instantiates a new SocketTCP given the address of the server and create the respective streams to 
	 * send and receive data.
	 * 
	 * @param serverAddr the address of the listen socket on the server.
	 */
	protected TCPSocket(InetSocketAddress serverAddr){
		_socketTcp = new Socket();

		try {
			_socketTcp.connect(serverAddr);
			_socketTcp.setSoTimeout(_timeout);
			System.out.println("TCP Connection Established with: " + serverAddr);
		} catch (IOException e) {
			System.out.println("Exception thrown trying to create client side TCPSocket: "+e.getMessage());
			e.printStackTrace();
		}

		try {
			_in = new DataInputStream(_socketTcp.getInputStream());
			_out = new DataOutputStream(_socketTcp.getOutputStream());
		} catch (IOException e) {
			System.out.println("Exception thrown creating the I\\O Streams: "+e.getMessage());
			e.printStackTrace();
		}		
	}

	public TCPSocket(InetSocketAddress serverAddr, int bindPort) {
		_socketTcp = new Socket();
		
		InetSocketAddress socketPort = new InetSocketAddress(bindPort);

		try {
			_socketTcp.bind(socketPort);
			_socketTcp.connect(serverAddr);
			_socketTcp.setSoTimeout(_timeout);
			System.out.println("TCP Connection Established with: " + serverAddr);
		} catch (IOException e) {
			System.out.println("Exception thrown trying to create client side TCPSocket: "+e.getMessage());
			e.printStackTrace();
		}

		try {
			_in = new DataInputStream(_socketTcp.getInputStream());
			_out = new DataOutputStream(_socketTcp.getOutputStream());
		} catch (IOException e) {
			System.out.println("Exception thrown creating the I\\O Streams: "+e.getMessage());
			e.printStackTrace();
		}
	}

	/**
	 * Given any object convert them to the canonical format and send them to the destination.
	 * 
	 * @param data the object to send
	 * 
	 * @throws ConnectionResetException when the connection broke during the transmission
	 */
	protected void send(Object data) throws ConnectionResetException {				
		try {
			byte[] serialized = Serialization.Serialize(data);
			_out.write(serialized);
		} catch(SocketException e){
			System.out.println("Exception thrown because connection finished");
			this.close();
			throw new ConnectionResetException();
		} catch (IOException e) {
			System.out.println("Exception trying to send TCP message: "+e.getMessage());
			e.printStackTrace();
		}
	}

	/**
	 * Receive data from the socket and convert them to a format application can deal with.
	 * 
	 * @return the object received from the socket
	 * 
	 * @throws ConnectionResetException if the connection broke during the receiving.
	 */
	protected Object receive() throws ConnectionResetException {
		Object data = null;
		try {
			byte[] received = new byte[_segmentSize];
			_in.read(received, 0, received.length);
			data = Serialization.Deserialize(received);
			return data;

		} catch (SocketTimeoutException e){
			return null;
		} catch(SocketException e){
			System.out.println("Exception thrown because connection finished");
			this.close();
			throw new ConnectionResetException();
		} catch (IOException e) {
			System.out.println("Exception trying to receive TCP message: "+e.getMessage());
			e.printStackTrace();
		} 
		return data;
	}

	/**
	 * Close the socket when him is no more needed.
	 */
	protected void close(){
		try {			
			_in.close();
			_out.close();
			_socketTcp.close();

		} catch (IOException e) {
			System.out.println("Exception thrown trying to close the streams and socket: "+e.getMessage());
			e.printStackTrace();
		}		
	}

	/**
	 * Gets the socket address on the client endpoint.
	 * 
	 * @return the InetSocketAdrress of the client endpoint.
	 */
	protected InetSocketAddress getSocketAddress(){
		InetSocketAddress srcAddr;
		srcAddr = new InetSocketAddress(_socketTcp.getLocalAddress(),_socketTcp.getLocalPort());
		return srcAddr;
	}

	/**
	 * Gets the socket address of the server endpoint.
	 * 
	 * @return the InetSocketAddress of the Server endpoint.
	 */
	protected InetSocketAddress getRemoteSocketAddress(){
		InetSocketAddress destAddr;
		destAddr = new InetSocketAddress(_socketTcp.getInetAddress(),_socketTcp.getPort());
		return destAddr;
	}
	
	protected boolean isConnected(){
		return _socketTcp.isConnected();
	}
}
